<?php

/**
 * slog专用函数
 *
 * @param        $log
 * @param string $type
 * @param string $css
 *
 * @return mixed
 * @throws Exception
 */
if (!function_exists('slog')) {
    function slog($log, $type = 'log', $css = '') {
        return call_user_func(array('SocketLog', $type), $log, $css);
    }
}



class SocketLog {
    public static $start_time = 0;
    public static $start_memory = 0;
    public static $port = 1116; //SocketLog 服务的http的端口号
    public static $log_types = array('log', 'info', 'error', 'warn', 'table', 'group', 'groupCollapsed', 'groupEnd', 'alert');

    protected static $_instance;

    public static $config = array(
        'enable' => true, //是否记录日志的开关
        'host' => '127.0.0.1',
        'optimize' => false,
        'show_included_files' => true,
        'error_handler' => true,
        'slog_post' => 1,
        //日志强制记录到配置的client_id
        'slog_client_id' => '',
        'force_client_id' => '',
        //限制允许读取日志的client_id
        'allow_client_ids' => array()
    );

    protected static $logs = array();

    protected static $css = array(
        'sql' => 'color:#009bb4;',
        'sql_warn' => 'color:#009bb4;font-size:14px;',
        'error_handler' => 'color:#f4006b;font-size:14px;',
        'page' => 'color:#40e2ff;background:#171717;'
    );


    public static function __callStatic($method, $args) {
        if (in_array($method, self::$log_types)) {
            array_unshift($args, $method);
            return call_user_func_array(array(self::getInstance(), 'record'), $args);
        }
    }
    /**
     * 接管报错
     */
    public static function registerErrorHandler() {
        set_error_handler(array(__CLASS__, 'error_handler'));
        set_exception_handler(array(__CLASS__, 'exception_handler'));
        register_shutdown_function(array(__CLASS__, 'fatalError'));
    }

    public static function error_handler($errno, $errstr, $errfile, $errline, $title = '') {
        switch ($errno) {
            case E_WARNING:
                $severity = 'E_WARNING';
                break;
            case E_NOTICE:
                $severity = 'E_NOTICE';
                break;
            case E_USER_ERROR:
                $severity = 'E_USER_ERROR';
                break;
            case E_USER_WARNING:
                $severity = 'E_USER_WARNING';
                break;
            case E_USER_NOTICE:
                $severity = 'E_USER_NOTICE';
                break;
            case E_STRICT:
                $severity = 'E_STRICT';
                break;
            case E_RECOVERABLE_ERROR:
                $severity = 'E_RECOVERABLE_ERROR';
                break;
            case E_DEPRECATED:
                $severity = 'E_DEPRECATED';
                break;
            case E_USER_DEPRECATED:
                $severity = 'E_USER_DEPRECATED';
                break;
            case E_ERROR:
                $severity = 'E_ERR';
                break;
            case E_PARSE:
                $severity = 'E_PARSE';
                break;
            case E_CORE_ERROR:
                $severity = 'E_CORE_ERROR';
                break;
            case E_COMPILE_ERROR:
                $severity = 'E_COMPILE_ERROR';
                break;
            case E_USER_ERROR:
                $severity = 'E_USER_ERROR';
                break;
            default:
                $severity = 'E_UNKNOWN_ERROR_' . $errno;
                break;
        }
        $msg = "{$severity}: {$errstr} in {$errfile} on line {$errline}";

        $title = is_string($title) ? $title : 'PHP ERROR';

        self::$logs[] = array(
            'type' => 'group',
            'msg' => $title,
            'css' => 'color:#f2f2f2;background:#d60000;'
        );
        self::$logs[] = array(
            'type' => 'log',
            'msg' => $msg,
            'css' => 'color:#d60000;'
        );
        self::$logs[] = array(
            'type' => 'groupEnd',
            'msg' => '',
            'css' => '',
        );

        if (php_sapi_name() == "cli") {
            // 表明当前运行方式是 cli-mode
            self::sendLog();
        }
    }

    public static function exception_handler($e) {

        $message = $e->getMessage() . "\r\n" . $e->getTraceAsString();
        self::error_handler($e->getCode(),  $message, $e->getFile(), $e->getLine(), 'PHP Exception');
        //self::sendLog(); //此类终止不会调用类的 __destruct 方法，所以此处手动sendLog
    }

    public static function fatalError() {
        // 保存日志记录
        if ($e = error_get_last()) {
            self::error_handler($e['type'], $e['message'], $e['file'], $e['line']);
            self::sendLog(); //此类终止不会调用类的 __destruct 方法，所以此处手动sendLog
        }
    }


    public static function getInstance() {


        if (self::$_instance === null) {

            self::$_instance = new self();
            // self::setConfig(Config::get('slog'));
            if (file_exists(__DIR__ . DIRECTORY_SEPARATOR . 'slog_config')) {
                $slog_config = json_decode(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'slog_config'), true);
                if ($slog_config) {
                    foreach ($slog_config as $key => $v) {
                        if (isset(self::$config[$key])) {
                            self::$config[$key] = $slog_config[$key];
                        }
                    }
                }
            }

            if (self::$config['error_handler']) {
                self::registerErrorHandler();
            }
        }
        return self::$_instance;
    }

    public function __destruct() {
        self::sendLog();
    }


    protected static function _log($type, $logs, $css = '') {
        self::getInstance()->record($type, $logs, $css);
    }


    protected static function check() {
        if (!self::getConfig('enable')) {
            return false;
        }
        $tabid = self::getClientArg('tabid');

        $slog_client_id = self::getConfig('slog_client_id');

        //是否记录日志的检查
        if (!$tabid && !$slog_client_id && !self::getConfig('force_client_id')) {
            return false;
        }
        //用户认证
        $allow_client_ids = self::getConfig('allow_client_ids');

        if (!empty($allow_client_ids)) {
            if (!in_array($slog_client_id, $allow_client_ids)) {
                return false;
            }
        }
        return true;
    }

    protected static function getClientArg($name) {
        static $args = array();

        $key = 'HTTP_USER_AGENT';

        if (isset($_SERVER['HTTP_SOCKETLOG'])) {
            $key = 'HTTP_SOCKETLOG';
        }

        if (!isset($_SERVER[$key])) {
            return null;
        }
        if (empty($args)) {
            if (!preg_match('/SocketLog\((.*?)\)/', $_SERVER[$key], $match)) {
                $args = array('tabid' => null);
                return null;
            }
            parse_str($match[1], $args);
        }
        if (isset($args[$name])) {
            return $args[$name];
        }
        return null;
    }


    //获得配置
    public static function getConfig($name) {
        if (isset(self::$config[$name]))
            return self::$config[$name];
        return null;
    }

    //记录日志
    public function record($type, $msg = '', $css = '') {
        if (!self::check()) {
            return;
        }

        self::$logs[] = array(
            'type' => $type,
            'msg' => $msg,
            'css' => $css
        );


        if (php_sapi_name() == "cli") {
            // 表明当前运行方式是 cli-mode
            self::sendLog();
        }
    }

    public static function sendLog() {

        if (!self::$logs || !self::check()) {
            return;
        }

        $end_time = microtime(true);
        $time_str = '';
        $memory_str = '';

        $runtime = self::$start_time ? $end_time - self::$start_time : $end_time - $_SERVER['REQUEST_TIME'];
        $reqs = number_format(1 / $runtime, 2);
        $time_str = "[运行时间：" . number_format($runtime, 2) . "s][吞吐率：{$reqs}req/s]";

        if (self::$start_memory) {
            $memory_use = number_format(memory_get_usage() - self::$start_memory / 1024, 2);
            $memory_str = "[内存消耗：{$memory_use}kb]";
        }

        if (isset($_SERVER['HTTP_HOST'])) {
            $current_uri =  $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
        } else {
            $current_uri = "cmd:" . implode(' ', $_SERVER['argv']);
        }
        array_unshift(self::$logs, array(
            'type' => 'group',
            'msg' =>  $current_uri . '|' . $time_str . $memory_str,
            'css' => self::$css['page']
        ));

        if (self::getConfig('show_included_files')) {

            self::$logs[] = array(
                'type' => 'group',
                'msg' => 'PHP INCLUDED FILES',
                'css' => 'color:#f2f2f2;background:#7d440e;'
            );
            self::$logs[] = array(
                'type' => 'log',
                'msg' => implode("\n", get_included_files()),
                'css' => 'color:#7d440e;'
            );
            self::$logs[] = array(
                'type' => 'groupEnd',
                'msg' => '',
                'css' => '',
            );
        }

        self::$logs[] = array(
            'type' => 'groupEnd',
            'msg' => '',
            'css' => '',
        );

        $tabid = self::getClientArg('tabid');

        $slog_client_id = self::getConfig('slog_client_id');

        $logs = array(
            'tabid' => $tabid,
            'slog_client_id' => $slog_client_id,
            'logs' => self::$logs,
            'force_client_id' => self::getConfig('force_client_id'),
        );

        $msg = @json_encode($logs);
        $address = '/' . $slog_client_id; //将client_id作为地址， server端通过地址判断将日志发布给谁
        self::send(self::getConfig('host'), $msg, $address);
        self::$logs = array();
    }


    /**
     * @param string   $host    - $host of socket server
     * @param string $message - 发送的消息
     * @param string $address - 地址
     *
     * @return bool
     */
    public static function send($host, $message = '', $address = '/') {
        static $Curl, $_flag;

        $host = explode(':', $host)[0];

        $url = 'http://' . $host . ':' . self::$port . $address;

        $Curl = $Curl ? $Curl : curl_init();

        if ($_flag == 1) {
            curl_setopt($Curl, CURLOPT_POSTFIELDS, $message);
            $response = curl_exec($Curl);
            return $response;
        } else {
            curl_setopt($Curl, CURLOPT_URL, $url);
            curl_setopt($Curl, CURLOPT_POST, true);
            curl_setopt($Curl, CURLOPT_POSTFIELDS, $message);
            curl_setopt($Curl, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($Curl, CURLOPT_CONNECTTIMEOUT, 1);
            curl_setopt($Curl, CURLOPT_TIMEOUT, 3);
            $headers = array(
                "Content-Type: application/json;charset=UTF-8"
            );
            curl_setopt($Curl, CURLOPT_HTTPHEADER, $headers); //设置header
            $response = curl_exec($Curl);
            $_flag = 1;
            return $response;
        }
    }

    static function listen() {
        if (!self::getInstance() && !self::check()) {
            return;
        }
        if (isset(self::$config['slog_post']) && self::$config['slog_post']) {
            //默认调试$_POST数据
            $data = ($_POST ?? file_get_contents("php://input"));
            slog($data);
        }
    }
}

$html = <<<EOF
<!DOCTYPE html>
<html lang="zh">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>在线调试</title>
    <!-- <link rel="stylesheet" href="app.css"> -->
    <style>
       *{padding:0;margin:0}body,html{position:relative;width:100%;height:100%}.status-1{background-color:#00e7b0 !important}.status-0{background-color:#000 !important}.status-2{background-color:#1a2dff !important}.toggle-config{display:none;width:30px;height:30px;background-color:#000;color:#fff;position:fixed;bottom:20px;right:20px;border-radius:100%;opacity:.6;z-index:99999;text-align:center;line-height:30px}.config{width:250px;height:calc(100% - 20px);display:inline-block;padding:10px;background-color:#ececec;cursor:pointer}.config .status{display:inline-block;width:15px;height:15px;border-radius:100%;float:right;margin-top:5px}.config .group-title{font-size:18px;margin-bottom:15px}.config .label{width:40px;display:inline-block}.config .group{display:block;margin-bottom:20px}.config .group *{font-size:14px}.config .group label{color:#666}.config .group input{padding:10px;border:1px solid #88ccbc;box-shadow:1px 1px 3px #eee;border-radius:5px}.config #clear{display:block;color:#fff;padding:10px 0;background-color:#88ccbc;border:none;text-align:center;border-radius:5px;cursor:pointer}.config #clear{background-color:#ef8181;margin-top:10px}#log-view{width:calc(100% - 270px);right:0;top:0;position:fixed;height:calc(100% - 20px);overflow-y:scroll}#log-view .right{float:right}#log-view .group{display:none;margin-bottom:10px}#log-view .group.on{display:block}#log-view .group h5{padding:5px;margin:0;cursor:pointer}#log-view .group pre{margin:0;margin-bottom:5px;white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;overflow:auto;word-break:break-all;word-wrap:break-word}#log-view .group .list{padding:10px;background-color:#f9f9f9;font-size:12px;display:none}#log-view .group .list.on{display:block}@media(max-width: 640px){.config{display:none}.config.on{display:block;position:absolute;z-index:99999}#log-view{width:100%;height:100%}.toggle-config{display:block}}
    </style>

</head>

<body x-data="app">
    <div class="toggle-config" :class="'status-'+status" @click="toggleConf()">M</div>
    <form class="config" :class="show_conf?'on':''">
        <h6 class="group-title">服务端配置 <span :class="'status status-'+status"></span></h6>
        <div class="group">
            <span class="label">全局:</span>
            <span>
                <input type="radio" name="enable" :checked="WS_CONF.enable===1" @click="WS_CONF.enable=1" value="1" />启用
                <input type="radio" name="enable" :checked="WS_CONF.enable===0" @click="WS_CONF.enable=0" value="0" />关闭
            </span>
        </div>

        <div class="group">
            <span class="label">wss:</span>
            <span>
                <input type="radio" name="wss" :checked="WS_CONF.wss===1" @click="WS_CONF.wss=1" value="1" />启用
                <input type="radio" name="wss" :checked="WS_CONF.wss===0" @click="WS_CONF.wss=0" value="0" />关闭
            </span>
        </div>

        <div class="group">
            <span class="label">地址:</span>
            <input type="text" x-model="WS_CONF.address" name="host">
        </div>
        <div class="group">
            <span class="label">用户:</span>
            <input type="text" name="slog_client_id" x-model="WS_CONF.slog_client_id">
        </div>

        <div class="group">
            <span class="label">POST:</span>
            <span>
                <input type="radio" name="slog_post" :checked="WS_CONF.slog_post===1" @click="WS_CONF.slog_post=1" value="1" />启用
                <input type="radio" name="slog_post" :checked="WS_CONF.slog_post===0" @click="WS_CONF.slog_post=0" value="0" />关闭
            </span>
        </div>

        <div class="group">
            <span class="label">文件:</span>
            <span>
                <input type="radio" :checked="WS_CONF.show_included_files===1" @click="WS_CONF.show_included_files=1"
                    name="show_included_files" value="1" />显示
                <input type="radio" :checked="WS_CONF.show_included_files===0" @click="WS_CONF.show_included_files=0"
                    name="show_included_files" value="0" />不显示
            </span>
        </div>

        <div class="group">
            <span class="label">异常:</span>
            <span>
                <input type="radio" :checked="WS_CONF.error_handler===1" @click="WS_CONF.error_handler=1"
                    name="error_handler" value="1" />接管
                <input type="radio" :checked="WS_CONF.error_handler===0" @click="WS_CONF.error_handler=0"
                    name="error_handler" value="0" />不接管
            </span>
        </div>

        <h6 class="group-title" style="margin-top: 50px;">前端配置</h6>
        <div class="group">
            <span class="label">滚动:</span>
            <span>
                <input type="radio" @click="APP_CONF.auto_scroll=1" name="auto_scroll"
                    :checked="APP_CONF.auto_scroll===1" value="1" />是
                <input type="radio" @click="APP_CONF.auto_scroll=0" name="auto_scroll"
                    :checked="APP_CONF.auto_scroll===0" value="0" />否
            </span>
        </div>
        <div class="group">
            <span class="label">展开:</span>
            <span>
                <input type="radio" name="expand" @click="APP_CONF.expand=1" :checked="APP_CONF.expand===1"
                    value="1" />是
                <input type="radio" name="expand" @click="APP_CONF.expand=0" :checked="APP_CONF.expand===0"
                    value="0" />否
            </span>
        </div>
        <div class="group">
            <span class="label">筛选:</span>
            <input type="text" name="filter" id="filter" x-model="APP_CONF.filter">
        </div>
        <span id="clear" @click="\$('#log-view').html('');">清除</span>
    </form>
    <div id="log-view"></div>
    <script src="https://unpkg.com/jquery@3.7.1/dist/jquery.js"></script>
    <script src="https://unpkg.com/js-cookie@3.0.5/dist/js.cookie.min.js"></script>
    <script>
        function app() {
            return {
                WS: null,
                connect_num: 0,
                status: 0,
                show_conf: 0,
                WS_CONF: {
                    wss: 0,
                    address: "127.0.0.1:1229",
                    slog_client_id: "",
                    enable: 1,
                    show_included_files: 0,
                    error_handler: 0,
                    slog_post:1
                },
                APP_CONF: { filter: "", auto_scroll: 1, expand: 1 },
                toggleConf() {
                    this.show_conf = !this.show_conf;
                },
                init() {
                    console.log('ws.ngrok.hdconn.cn');
                    this.read_conf();
                    let _this = this;
                    this.\$watch("WS_CONF", function (newValue, oldValue) {
                        _this.updateConfig();
                    });
                    this.\$watch("APP_CONF", function (newValue, oldValue) {
                        var f = newValue["filter"].trim();
                        localStorage.setItem("APP_CONF", JSON.stringify(newValue));
                        if (!f) {
                            \$("#log-view .group").show();
                        } else {
                            \$("#log-view .group").each(function () {
                                if (\$(this).data("url").indexOf(f) > -1) {
                                    \$(this).show();
                                } else {
                                    \$(this).hide();
                                }
                            });
                        }
                    });
                    _this.updateConfig();

                },
                updateConfig() {
                    let _this = this;
                    localStorage.setItem("WS_CONF", JSON.stringify(_this.WS_CONF));
                    Cookies.set("slog_client_id",_this.WS_CONF.slog_client_id);
                    $.post(
                        "?slog=1&save=1",
                        this.WS_CONF,
                        function (data, textStatus, xhr) {
                            _this.reconnect();
                        },
                        "json",
                    ).fail(function() {
                        _this.reconnect();
                    });
                },
                reconnect() {
                    this.connect_num = 0;
                    this.status=0;
                    try {
                        this.WS.onopen = null;
                        this.WS.onmessage = null;
                        this.WS.onclose = null;
                        this.WS.onerror = null;
                        this.WS.close();
                        this.WS = null;
                        delete this.WS;
                    } catch (e) { }
                    let _this = this;
                    setTimeout(function () {
                        _this.initSocket();
                    }, 1000);
                },
                initSocket() {
                    let _this = this;
                    if (this.connect_num > 0 || !this.WS_CONF.enable) {
                        return false;
                    }
                    this.WS = new WebSocket(
                        (this.WS_CONF.wss ? "wss" : "ws") +
                        "://" +
                        this.WS_CONF.address +
                        "/" +
                        this.WS_CONF.slog_client_id,
                    );
                    this.WS.onopen = function () {
                        _this.status = 1;
                    };
                    this.WS.onmessage = function (event) {
                        try {
                            var data = JSON.parse(event.data);
                        } catch (e) {
                            return alert("日志格式错误，" + event.data);
                        }
                        var html = "";
                        for (var i in data.logs) {
                            var log = data["logs"][i];
                            if (log.type == "group") {
                                log.msg = log.msg.split("|");
                                html +=
                                    '<div class="group ' +
                                    (!_this.APP_CONF.filter ||
                                        (_this.APP_CONF.filter &&
                                            log["msg"].indexOf(_this.APP_CONF.filter) > -1)
                                        ? "on"
                                        : "") +
                                    '" data-url="'+log.msg[0]+'" data-client="' +
                                    data.slog_client_id +
                                    '"><h5 style="' +
                                    log.css +
                                    '"><span class="uri">' +
                                    log.msg[0] +
                                    '</span><span class="right">' +
                                    (log.msg[1] || "") +
                                    "</span></h5>";
                                html +=
                                    _this.APP_CONF.expand == 1
                                        ? '<div class="list on">'
                                        : '<div class="list">';
                            } else if (log.type == "groupEnd") {
                                html += "</div></div>";
                            } else {
                                if (typeof log.msg == "object") {
                                    log.msg = JSON.stringify(log.msg, null, 2);
                                }
                                html += '<pre style="' + log.css + '">' + log.msg + "</pre>";
                            }
                            if (log.css) {
                                console[log.type]("%c" + log.msg, log.css);
                            } else {
                                console[log.type](log.msg);
                            }
                        }
                        \$("#log-view").append(html);
                        if (_this.APP_CONF.auto_scroll && _this.APP_CONF.auto_scroll == 1) {
                            \$("#log-view").scrollTop(\$("#log-view")[0].scrollHeight);
                        }
                    };
                    this.WS.onerror = function (e) {
                        console.log("error", e);
                        _this.status = 0;
                        _this.reconnect();
                    };
                    this.WS.onclose = function () {
                        _this.status = 0;
                        _this.reconnect();
                    };
                },
                read_conf() {
                    var config = localStorage.getItem("WS_CONF");
                    if (config) {
                        this.WS_CONF = JSON.parse(config);
                    }
                    config = localStorage.getItem("APP_CONF");
                    if (config) {
                        this.APP_CONF = JSON.parse(config);
                    }

                    var slog_client_id=Cookies.get("slog_client_id");
                    if(slog_client_id){
                        this.WS_CONF.slog_client_id=slog_client_id;
                    }
                },
            };
        }
        \$("body").on("click", "h5", function () {
            \$(this).siblings(".list").toggleClass("on");
        });
    </script>

    <script src="https://unpkg.com/alpinejs@3.14.1/dist/cdn.min.js"></script>
</body>

</html>
EOF;


if (isset($_GET['slog'])) {

    $slog_client_id = $_COOKIE['slog_client_id'] ?? uniqid();

    if (!isset($_COOKIE['slog_client_id']) || !$_COOKIE['slog_client_id']) {
        setcookie('slog_client_id', $slog_client_id, time() + 3600 * 24 * 30);
    }

    if (isset($_GET['save'])) {
        file_put_contents(__DIR__ . DIRECTORY_SEPARATOR . 'slog_config', json_encode($_POST, JSON_UNESCAPED_UNICODE));
        die(json_encode(array('status' => 1)));
    }
    die($html);
}


SocketLog::listen();
